﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Collections.Generic;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;
    using System.Net;
#if !WINDOWS_PHONE
    using System.Runtime.Serialization.Formatters.Binary;
#endif
    using System.Threading;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    using Phuse.Mime;
    using Phuse.Net.Nntp;

    /// <summary>Tests the <see cref="Cache"/> class with a real server.</summary>
    [TestClass]
    public sealed class ServerTest : TestInfrastructure
    {
        /// <summary>Tests the <see cref="Cache"/> creation use cases.</summary>
        [TestMethod]
        public void CreationTest()
        {
            AssertThrow<ArgumentException>(() => new Cache("<", new NullCryptoProvider()).Dispose());
            AssertThrow<ArgumentNullException>(() => new Cache(null, new NullCryptoProvider()).Dispose());
            AssertThrow<FileNotFoundException>( // non-existent directory
                () => new Cache(Path.Combine(OurDirectory, GetUniqueString()), new NullCryptoProvider()).Dispose());

            using (var provider = new FileIOProvider(OurDirectory))
            {
                // non-existent DB file
                AssertThrow<FileNotFoundException>(
                    () => new Cache(CreateUniqueDirectory(provider), new NullCryptoProvider()).Dispose());
                AssertThrow<ArgumentNullException>(() => new Cache(CreateUniqueDirectory(provider), null).Dispose());
            }

            AssertThrow<ArgumentException>(() => Cache.CreateCache(OurDirectory)); // Non-empty directory
            AssertThrow<ArgumentException>(() => Cache.CreateCache("*?"));
            AssertThrow<ArgumentNullException>(() => Cache.CreateCache(null));

            string tooLong = OurDirectory;

            while (tooLong.Length <= 248)
            {
                tooLong = Path.Combine(tooLong, GetUniqueString());
            }

            AssertThrow<IOException>(() => Cache.CreateCache(tooLong));

            using (var cache = CreateNewCache())
            {
                this.AddRandomHosts(cache);
            }

            this.TestHostAdditionExceptions();
            this.TestGroupAdditionExceptions();
        }

        /// <summary>Tests the <see cref="Cache"/> modification use cases.</summary>
        [TestMethod]
        public void ModificationTest()
        {
            this.VerifyMaxLength((host, s) => host.UserName = s, cache => cache.UserNameMaxLength);
            this.VerifyMaxLength((host, s) => host.EmailAddress = s, cache => cache.EmailAddressMaxLength);
            this.VerifyMaxLength((host, s) => host.AccountName = s, cache => cache.AccountNameMaxLength);
            this.VerifyMaxLength((host, s) => host.Password = s, cache => cache.PasswordMaxLength);

            this.VerifyMaxLength((group, s) => group.DisplayName = s, cache => cache.GroupDisplayNameMaxLength);
            this.AssertThrow<ArgumentNullException>((cache, group) =>
                {
                    group.DisplayName = null;
                    cache.SaveChanges();
                });
        }

        /// <summary>Tests auxiliary members of classes associated with <see cref="Cache"/>.</summary>
        [TestMethod]
        public void AuxiliaryMembersTest()
        {
            const int NewInboundArticlesReceiveLimit = 10;
            const int CachedInboundArticlesLimit = 10;

            using (var cache = CreateNewCache())
            {
                var host1 = CreateGmaneHost(cache);
                var group1 =
                    CreateBoostDeveloperGroup(cache, host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var group1Read = host1.Groups.First();
                var group2 = CreateGccDeveloperGroup(
                    cache, host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);

                var observer = new ConsoleProgressObserver();
                var task = cache.SendReceiveAsync(new[] { group1, group2 }, false, CancellationToken.None, observer);

                EqualsGetHashCodeTest(group1, group1Read, group2);

                WaitForSendReceiveToFinish(cache, task, observer);
                observer.AssertNoExceptions();

                group1 = Refresh(cache, group1);
                EqualsGetHashCodeTest(
                    group1.GetOriginalInboundArticles(group1.InboundArticleFilter).First(),
                    group1.GetOriginalInboundArticles(group1.InboundArticleFilter).First(),
                    group1.GetOriginalInboundArticles(group1.InboundArticleFilter).ElementAt(1));
                AssertReadStatus(group1.GetOriginalInboundArticles(group1.InboundArticleFilter), false);
                group1.MarkAllInboundArticlesAsRead();
                Assert.IsTrue(cache.SaveChanges());
                group1 = Refresh(cache, group1);
                AssertReadStatus(group1.GetOriginalInboundArticles(group1.InboundArticleFilter), true);
            }
        }

        /// <summary>Tests the main cache use cases with real servers.</summary>
        [TestMethod]
        public void MainTest()
        {
            const int NewInboundArticlesReceiveLimit = 60;
            const int CachedInboundArticlesLimit = 30;
            string dataDirectory;
            int group1ArticleCount;

            using (var cache = CreateNewCache())
            {
                dataDirectory = cache.DataDirectory;
                var host1 = CreateGmaneHost(cache);
                var group1 = CreateBoostDeveloperGroup(
                    cache, host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var group2 = CreateGccDeveloperGroup(
                    cache, host1, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);
                var host2 = CreateTestHost(cache);
                var group3 = CreateTestGroup(cache, host2, NewInboundArticlesReceiveLimit, CachedInboundArticlesLimit);

                TestOutboundArticles(cache, host2, group2, group3);
                TestStartStopSendReceive(cache, Refresh(cache, host1), Refresh(cache, group1), Refresh(cache, group3));
                TestQueries(
                    NewInboundArticlesReceiveLimit,
                    cache,
                    Refresh(cache, group1),
                    Refresh(cache, group2),
                    Refresh(cache, group3));

                Refresh(cache, group2).Delete();
                Assert.IsTrue(cache.SaveChanges());
                Assert.AreEqual(Refresh(cache, host1).Groups.Count(), 1);
                Refresh(cache, host2).Delete();
                Assert.IsTrue(cache.SaveChanges());
                Assert.AreEqual(cache.Hosts.Count(), 1);

                group1 = Refresh(cache, group1);
                group1ArticleCount = GetArticleCount(group1);

                // Ensure that the next connection to the Cache will delete old articles
                group1.MarkAllInboundArticlesAsRead();
                Assert.IsTrue(cache.SaveChanges());
            }

            TestCompaction(dataDirectory, group1ArticleCount);
        }

        /// <summary>Tests whether the data read from the DB is the same as the data stored in objects returned by Add*
        /// methods.</summary>
        [TestMethod]
        public void AddAndReadTest()
        {
            using (var cache = CreateNewCache())
            {
                var host = this.AddRandomHost(cache);
                var readHost = cache.Hosts.First();
                Assert.AreEqual(host.Name, readHost.Name);
                Assert.AreEqual(host.Port, readHost.Port);
                Assert.AreEqual(host.UserName, readHost.UserName);
                Assert.AreEqual(host.EmailAddress, readHost.EmailAddress);
                Assert.AreEqual(host.AccountName, readHost.AccountName);
                Assert.AreEqual(host.Password, readHost.Password);

                var outboundArticle = host.AddOutboundArticle();
                cache.SaveChanges();
                var message = outboundArticle.LoadMessage();
                Assert.AreEqual(1, message.Fields.Count); // MIME-Version is always present
                Assert.IsNull(message.Body);
                var readOutboundArticle = cache.GetOutboundArticles(SendStatus.Draft).First();
                Assert.AreEqual(outboundArticle.SendStatus, readOutboundArticle.SendStatus);

                var group = this.AddRandomGroup(cache, host);
                var readGroup = host.Groups.First();
                Assert.AreEqual(group.Name, readGroup.Name);
                Assert.AreEqual(group.DisplayName, readGroup.DisplayName);
                Assert.AreEqual(group.NewInboundArticlesReceiveLimit, readGroup.NewInboundArticlesReceiveLimit);
                Assert.AreEqual(group.CachedInboundArticlesLimit, readGroup.CachedInboundArticlesLimit);
                Assert.AreEqual(group.InboundArticleFilter, readGroup.InboundArticleFilter);
            }
        }

        /// <summary>Tests what happens when a server does not respond to the initial connection attempt.</summary>
        [TestMethod]
        public void HostUnreachableTest()
        {
            using (var cache = CreateNewCache())
            {
                var observer = new ConsoleProgressObserver();
                WaitForSendReceiveToFinish(cache, Receive(cache, true, observer), observer);
                observer.AssertEventCount(typeof(ConnectProgress), 1);
                observer.AssertCallCount(new HostUnreachableException(), 1);
                observer.AssertTotalEventCount(3);
            }
        }

        /// <summary>Tests all cache related exceptions.</summary>
        [TestMethod]
        public void ExceptionTest()
        {
            TestStdExceptionConstructors<ConnectionLostException>();
            TestStdExceptionConstructors<HostUnreachableException>();
            TestStdExceptionConstructors<IncompatibleCacheException>();
            TestStdExceptionConstructors<MissingComponentException>();
            TestStdExceptionConstructors<WriteFailedException>();

            TestSerialization<ConnectionLostException>();
            TestSerialization<HostUnreachableException>();
            TestSerialization<IncompatibleCacheException>();
            TestSerialization<MissingComponentException>();
            TestSerialization<WriteFailedException>();
        }

        /// <summary>Tests the exception thrown from a cache with an unexpected number of rows in the Root table.
        /// </summary>
        [TestMethod]
        public void UnexpectedRootRowCountTest()
        {
            string dataDirectory;

            using (var cache = CreateNewCache())
            {
                dataDirectory = cache.DataDirectory;
            }

            using (var provider = new FileIOProvider(dataDirectory))
            using (var context = new DataContext(provider.GetDatabaseConnectionString("Cache.sdf")))
            {
                context.GetTable<FakeRoot>().InsertOnSubmit(new FakeRoot());
                context.SubmitChanges();
            }

            AssertThrow<IncompatibleCacheException>(
                () => new Cache(dataDirectory, new NullCryptoProvider()).Dispose());
        }

        /// <summary>Tests all the cases where an <see cref="IncompatibleCacheException"/> is thrown.</summary>
        [TestMethod]
        public void IncompatibleCacheTest()
        {
            var thisAssembly = typeof(ServerTest).Assembly;
            var prefix = typeof(ServerTest).Namespace + ".";
            const string Extension = ".sdf";

            foreach (var databaseName in thisAssembly.GetManifestResourceNames())
            {
                if (databaseName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) &&
                    databaseName.EndsWith(Extension, StringComparison.OrdinalIgnoreCase))
                {
                    string dataDirectory;

                    using (var provider = new FileIOProvider(OurDirectory))
                    {
                        dataDirectory = CreateUniqueDirectory(provider);
                        var destinationPath = Path.Combine(dataDirectory, "Cache.sdf");

                        using (var source = thisAssembly.GetManifestResourceStream(databaseName))
                        using (var destination = provider.OpenFile(
                            destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                        {
                            source.CopyTo(destination);
                        }
                    }

                    if (databaseName.Contains(".Correct.sdf") || databaseName.Contains(".VersionMissing.sdf"))
                    {
                        new Cache(Path.Combine(OurDirectory, dataDirectory), new NullCryptoProvider()).Dispose();
                    }
                    else
                    {
                        AssertThrow<IncompatibleCacheException>(
                            () => new Cache(Path.Combine(OurDirectory, dataDirectory), new NullCryptoProvider()).Dispose());
                    }
                }
            }
        }

        /// <summary>Tests performance of SendReceiveAsync.</summary>
        [TestMethod]
        public void SendReceivePerformanceTest()
        {
            using (var cache = CreateNewCache())
            {
                var host = CreateGmaneHost(cache);
                var group = CreateBoostDeveloperGroup(cache, host, 300, 300);
                cache.SaveChanges();

                var observer = new ConsoleProgressObserver();

                using (var task = cache.SendReceiveAsync(new[] { group }, true, CancellationToken.None, observer))
                {
                    task.Wait();
                }

                observer.AssertEventCount(typeof(ConnectProgress), 2);
                observer.AssertEventCount(typeof(HeaderReceiveProgress), 101);
                observer.AssertEventCount(typeof(ContentReceiveProgress), 101);
                observer.AssertTotalEventCount(204);
            }
        }

#if !WINDOWS_PHONE
        /// <summary>Tests the <see cref="IForumProvider{T}"/> implementations.</summary>
        [TestMethod]
        public void ProviderTest()
        {
            using (var cache = CreateNewCache())
            {
                var host = cache.AddHost<YouTubeForumProvider, YouTubeConnection>("youtube.com", 80);
                var group = host.AddGroup("JsD6uEZsIsU", "whatever", 500, 500);
                cache.SaveChanges();

                var observer = new ConsoleProgressObserver();

                using (var task = cache.SendReceiveAsync(new[] { group }, true, CancellationToken.None, observer))
                {
                    task.Wait();
                }
            }
        }
#endif

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void AddRandomHosts(Cache cache)
        {
            var hostCount = Random.Next(1, 10);

            for (var i = 0; i < hostCount; ++i)
            {
                this.AddRandomGroups(cache, this.AddRandomHost(cache));
            }

            Assert.AreEqual(hostCount, cache.Hosts.Count());
            cache.Hosts.First().Delete();
            Assert.IsTrue(cache.SaveChanges());
            Assert.AreEqual(hostCount - 1, cache.Hosts.Count());
        }

        private void TestHostAdditionExceptions()
        {
            AssertThrow<ArgumentNullException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(null, 0);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(new string('x', cache.HostNameMaxLength + 1), 0);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentOutOfRangeException>(
                cache => cache.AddHost<NntpForumProvider, NntpConnection>(GetUniqueString(), IPEndPoint.MinPort - 1),
                cache => cache.AddHost<NntpForumProvider, NntpConnection>(GetUniqueString(), IPEndPoint.MaxPort + 1));
            AssertThrow<ArgumentException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(
                        GetUniqueString(), 119, new string('x', cache.UserNameMaxLength + 1), null, null, null);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(
                        GetUniqueString(), 119, null, new string('x', cache.EmailAddressMaxLength + 1), null, null);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(
                        GetUniqueString(),
                        119,
                        null,
                        null,
                        new string('x', cache.AccountNameMaxLength + 1),
                        string.Empty);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentException>(cache =>
                {
                    cache.AddHost<NntpForumProvider, NntpConnection>(
                        GetUniqueString(), 119, null, null, string.Empty, new string('x', cache.PasswordMaxLength + 1));
                    cache.SaveChanges();
                });

            using (var cache = CreateNewCache())
            {
                this.AddRandomHost(cache);
                var firstHost = cache.Hosts.First();
                AssertThrow<ArgumentException>(() =>
                    {
                        cache.AddHost<NntpForumProvider, NntpConnection>(firstHost.Name, firstHost.Port);
                        cache.SaveChanges();
                    });
            }
        }

        private void AddRandomGroups(Cache cache, IHost host)
        {
            var groupCount = Random.Next(1, 10);

            for (int i = 0; i < groupCount; ++i)
            {
                this.AddRandomGroup(cache, host);
            }

            Assert.AreEqual(groupCount, host.Groups.Count());
            host.Groups.First().Delete();
            Assert.IsTrue(cache.SaveChanges());
            Assert.AreEqual(groupCount - 1, cache.Hosts.Single(h => h.Name == host.Name).Groups.Count());
        }

        private void TestGroupAdditionExceptions()
        {
            AssertThrow<ArgumentNullException>(
                (cache, host) =>
                {
                    host.AddGroup(null, "x", 1, 1);
                    cache.SaveChanges();
                },
                (cache, host) =>
                {
                    host.AddGroup("x", null, 1, 1);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentException>(
                (cache, host) =>
                {
                    host.AddGroup(new string('x', cache.GroupNameMaxLength + 1), "x", 1, 1);
                    cache.SaveChanges();
                },
                (cache, host) =>
                {
                    host.AddGroup("x", new string('x', cache.GroupDisplayNameMaxLength + 1), 1, 1);
                    cache.SaveChanges();
                });
            AssertThrow<ArgumentOutOfRangeException>(
                (cache, host) => host.AddGroup("x", "x", 0, 1),
                (cache, host) => host.AddGroup("x", "x", 1, 0));

            using (var cache = CreateNewCache())
            {
                var newHost = this.AddRandomHost(cache);
                this.AddRandomGroup(cache, newHost);
                var firstGroup = cache.Hosts.First().Groups.First();
                AssertThrow<ArgumentException>(() =>
                    {
                        newHost.AddGroup(firstGroup.Name, firstGroup.DisplayName, 1, 1);
                        cache.SaveChanges();
                    });
            }
        }

        private void VerifyMaxLength(Action<IGroup, string> setProperty, Func<Cache, int> getMaxLength)
        {
            using (var cache = CreateNewCache())
            {
                setProperty(AddRandomGroup(cache, AddRandomHost(cache)), new string('x', getMaxLength(cache)));
                cache.SaveChanges();
            }

            AssertThrow<ArgumentException>((cache, group) =>
                {
                    setProperty(group, new string('x', getMaxLength(cache) + 1));
                    cache.SaveChanges();
                });
        }

        private void VerifyMaxLength(Action<IHost, string> setProperty, Func<Cache, int> getMaxLength)
        {
            using (var cache = CreateNewCache())
            {
                setProperty(AddRandomHost(cache), new string('x', getMaxLength(cache)));
                cache.SaveChanges();
            }

            AssertThrow<ArgumentException>((cache, host) =>
                {
                    setProperty(host, new string('x', getMaxLength(cache) + 1));
                    cache.SaveChanges();
                });
        }

        private void AssertThrow<TException>(params Action<Cache, IGroup>[] actions) where TException : Exception
        {
            foreach (var action in actions)
            {
                this.AssertThrow<TException>(
                    (cache, host) =>
                    {
                        var group = this.AddRandomGroup(cache, host);
                        action(cache, group);
                    });
            }
        }

        private void AssertThrow<TException>(params Action<Cache, IHost>[] actions) where TException : Exception
        {
            foreach (var action in actions)
            {
                ServerTest.AssertThrow<TException>(
                    cache =>
                    {
                        var host = this.AddRandomHost(cache);
                        action(cache, host);
                    });
            }
        }

        private static void TestSerialization<TException>() where TException : Exception, new()
        {
#if !WINDOWS_PHONE
            using (var stream = new MemoryStream())
            {
                var serializer = new BinaryFormatter();
                serializer.Serialize(stream, new TException());
                stream.Seek(0, SeekOrigin.Begin);
                serializer.Deserialize(stream);
            }
#endif
        }

        private static void AssertThrow<TException>(params Action<Cache>[] actions) where TException : Exception
        {
            foreach (var action in actions)
            {
                using (var cache = CreateNewCache())
                {
                    AssertThrow<TException>(() => action(cache));
                }
            }
        }

        private static void EqualsGetHashCodeTest<T>(T object1, T object1Clone, T object2)
        {
            Assert.IsTrue(((object)object1).Equals(object1Clone));
            Assert.IsFalse(((object)object1).Equals(new object()));
            Assert.IsTrue(object1.Equals(object1Clone));
            Assert.IsFalse(object1.Equals(null));
            Assert.IsFalse(object1.Equals(object2));
            Assert.AreEqual(object1.GetHashCode(), object1Clone.GetHashCode());
            Assert.AreNotEqual(object1.GetHashCode(), object2.GetHashCode());
        }

        private static void AssertReadStatus(IEnumerable<IInboundArticle> articles, bool read)
        {
            foreach (var article in articles)
            {
                Assert.AreEqual(read, article.HasBeenRead);
                AssertReadStatus(article.GetFollowupInboundArticles(), read);
            }
        }

        private static void TestStartStopSendReceive(Cache cache, IHost host1, IGroup group1, IGroup group3)
        {
            AssertThrow<ArgumentNullException>(() => cache.SendReceiveAsync(
                null, false, CancellationToken.None, new ConsoleProgressObserver()).Dispose());

            var observer = new ConsoleProgressObserver();

            // Start/stop send/receive
            using (var source = new CancellationTokenSource())
            using (var task = cache.SendReceiveAsync(new[] { group3 }, true, source.Token, observer))
            {
                AssertThrow<InvalidOperationException>(
                    () => cache.SendReceiveAsync(new[] { group3 }, true, CancellationToken.None, observer).Dispose(),
                    () => cache.Compact(),
                    () =>
                    {
                        group1.Delete();
                        cache.SaveChanges();
                    },
                    () =>
                    {
                        cache.Reset();
                        Refresh(cache, host1).Delete();
                        cache.SaveChanges();
                    },
                    () =>
                    {
                        AddOutboundArticle(cache, host1).SendStatus = SendStatus.Outbox;
                        cache.SaveChanges();
                    },
                    () =>
                    {
                        AddOutboundArticle(cache, host1).IsDeleted = true;
                        cache.SaveChanges();
                    },
                    () =>
                    {
                        AddOutboundArticle(cache, host1).SaveMessage(
                            new Message() { From = new[] { new Address("Me", "me@hotmail.com") } });
                    });

                Thread.Sleep(1000); // Ensure that the send/receive thread had a chance to start
                source.Cancel();
                WaitForSendReceiveToFinish(cache, task, observer);
                Assert.IsTrue(task.IsCanceled);
            }

            cache.Reset();
        }

        private static IOutboundArticle AddOutboundArticle(Cache cache, IHost host1)
        {
            cache.Reset();
            var outboundArticle = Refresh(cache, host1).AddOutboundArticle();
            cache.SaveChanges();
            return outboundArticle;
        }

        private static void TestOutboundArticles(Cache cache, IHost host2, IGroup group2, IGroup group3)
        {
            var outboundArticle = host2.AddOutboundArticle();
            cache.SaveChanges();
            AssertThrow<ArgumentException>(() => outboundArticle.SendStatus = SendStatus.Sent);
            Assert.AreEqual(SendStatus.Draft, outboundArticle.SendStatus);
            AssertOutboundCounts(cache, 1, 0, 0);
            AssertThrow<ArgumentNullException>(() => outboundArticle.SaveMessage(null));

            Assert.AreEqual(host2, outboundArticle.Host);

            var message = CreateTestMessage();
            outboundArticle.SaveMessage(message);
            cache.SaveChanges();
            Assert.IsNotNull(message.Date);
            Assert.IsNotNull(message.MessageId);
            Assert.AreEqual(message.Date, outboundArticle.DateUtc);
            Assert.AreEqual(message.Subject, outboundArticle.Subject);
            var date = message.Date;
            var messageId = message.MessageId;

            Assert.AreEqual(SendStatus.Draft, outboundArticle.SendStatus);
            outboundArticle.SendStatus = SendStatus.Outbox;
            Assert.AreEqual(SendStatus.Outbox, outboundArticle.SendStatus);
            cache.SaveChanges();
            AssertOutboundCounts(cache, 0, 1, 0);

            var loadedMessage = outboundArticle.LoadMessage();
            Assert.IsTrue(TimeSpan.FromSeconds(1) > date - loadedMessage.Date);
            Assert.AreEqual(messageId, loadedMessage.MessageId);
            outboundArticle.SaveMessage(loadedMessage);
            cache.SaveChanges();
            loadedMessage = outboundArticle.LoadMessage();
            Assert.AreEqual(messageId, loadedMessage.MessageId);
            loadedMessage.From[0] = new Address(
                loadedMessage.From[0].DisplayName.Replace('r', 'l'), loadedMessage.From[0].EmailAddress);
            outboundArticle.SaveMessage(loadedMessage);
            var messageId2 = loadedMessage.MessageId;
            Assert.AreNotEqual(messageId, messageId2);
            loadedMessage.From[0] = new Address(
                loadedMessage.From[0].DisplayName.Replace('l', 'r'), loadedMessage.From[0].EmailAddress);
            outboundArticle.SaveMessage(loadedMessage);
            Assert.AreNotEqual(messageId2, loadedMessage.MessageId);

            var outboundArticle2 = host2.AddOutboundArticle();
            cache.SaveChanges();
            var message2 = CreateTestMessage();
            message2.Body = null;
            outboundArticle2.SaveMessage(message2);
            outboundArticle2.SendStatus = SendStatus.Outbox;
            cache.SaveChanges();
            AssertOutboundCounts(cache, 0, 2, 0);

            var observer = new ConsoleProgressObserver();
            WaitForSendReceiveToFinish(
                cache,
                cache.SendReceiveAsync(new[] { group2, group3 }, false, CancellationToken.None, observer),
                observer);
            observer.AssertCallCount(new UnknownCommandException(), 1);
            AssertOutboundCounts(cache, 0, 1, 1);
            var unsentArticle = cache.GetOutboundArticles(SendStatus.Outbox).First();
            unsentArticle.SendStatus = SendStatus.Draft;
            cache.SaveChanges();
            var sentArticle = cache.GetOutboundArticles(SendStatus.Sent).First();
            AssertThrow<InvalidOperationException>(() => sentArticle.SaveMessage(new Message()));
            AssertThrow<InvalidOperationException>(() => sentArticle.SendStatus = SendStatus.Outbox);
            AssertOutboundCounts(cache, 1, 0, 1);
            Assert.IsFalse(unsentArticle.IsDeleted);
            unsentArticle.IsDeleted = true;
            Assert.IsTrue(unsentArticle.IsDeleted);
            sentArticle.IsDeleted = true;
            cache.SaveChanges();
            Assert.AreEqual(2, cache.GetDeletedOutboundArticles().Count());
            AssertOutboundCounts(cache, 0, 0, 0);
            unsentArticle.IsDeleted = false;
            sentArticle.IsDeleted = false;
            cache.SaveChanges();
            AssertOutboundCounts(cache, 1, 0, 1);
        }

        private static void AssertOutboundCounts(Cache cache, int draft, int outbox, int sent)
        {
            Assert.AreEqual(draft, cache.GetOutboundArticles(SendStatus.Draft).Count());
            Assert.AreEqual(outbox, cache.GetOutboundArticles(SendStatus.Outbox).Count());
            Assert.AreEqual(sent, cache.GetOutboundArticles(SendStatus.Sent).Count());
            var counts = cache.GetOutboundArticleCounts();
            Assert.AreEqual(draft, counts[SendStatus.Draft]);
            Assert.AreEqual(outbox, counts[SendStatus.Outbox]);
            Assert.AreEqual(sent, counts[SendStatus.Sent]);
        }

        private static void TestQueries(
            int newInboundArticlesReceiveLimit,
            Cache cache,
            IGroup group1,
            IGroup group2,
            IGroup group3)
        {
            group2.InboundArticleFilter = new InboundArticleFilter(true, true, true, true);
            Assert.IsTrue(
                group2.GetOriginalInboundArticleCount(new InboundArticleFilter(null, null, null, null)) > 0);
            Assert.AreEqual(0, group2.GetOriginalInboundArticles(group2.InboundArticleFilter).Count());
            Assert.AreEqual(0, group2.GetOriginalInboundArticleCount(group2.InboundArticleFilter));
            group2.InboundArticleFilter = new InboundArticleFilter(null, null, null, null);
            Assert.AreEqual(
                0, group2.GetOriginalInboundArticleCount(new InboundArticleFilter(true, true, true, true)));
            var articles =
                group2.GetOriginalInboundArticles(new InboundArticleFilter(null, null, null, null)).ToList();
            Assert.IsTrue(group2.GetOriginalInboundArticles(group2.InboundArticleFilter).SequenceEqual(articles));
            Assert.AreEqual(articles.Count, group2.GetOriginalInboundArticleCount(group2.InboundArticleFilter));

            Assert.IsTrue(group2.GetOriginalInboundArticles(group2.InboundArticleFilter).SequenceEqual(
                group2.GetOriginalInboundArticles(new InboundArticleFilter(null, null, null, null))));
            Assert.IsTrue(group2.GetOriginalInboundArticles(group2.InboundArticleFilter).SequenceEqual(
                group2.GetOriginalInboundArticles(new InboundArticleFilter(false, null, false, null))));
            Assert.IsTrue(group2.GetOriginalInboundArticles(group2.InboundArticleFilter).SequenceEqual(
                group2.GetOriginalInboundArticles(new InboundArticleFilter(null, false, null, false))));
            Assert.IsTrue(group2.GetOriginalInboundArticles(group2.InboundArticleFilter).SequenceEqual(
                group2.GetOriginalInboundArticles(new InboundArticleFilter(false, false, false, false))));
            Assert.AreEqual(
                0, group2.GetOriginalInboundArticles(new InboundArticleFilter(null, true, null, true)).Count());
            Assert.AreEqual(
                0,
                group2.GetOriginalInboundArticles(new InboundArticleFilter(false, true, false, true)).Count());
            Assert.AreEqual(
                0, group2.GetOriginalInboundArticles(new InboundArticleFilter(true, null, true, null)).Count());
            Assert.AreEqual(
                0,
                group2.GetOriginalInboundArticles(new InboundArticleFilter(true, false, true, false)).Count());
            Assert.AreEqual(
                0, group2.GetOriginalInboundArticles(new InboundArticleFilter(true, true, true, true)).Count());
            int group2CountBefore = TestArticles(
                group2, group2.GetOriginalInboundArticles(group2.InboundArticleFilter), false, false);
            cache.SaveChanges();
            Assert.IsTrue((group2CountBefore > 0) && (group2CountBefore <= newInboundArticlesReceiveLimit));

            // Receive full article content, ensure that previously marked articles are received too
            var observer = new ConsoleProgressObserver();
            WaitForSendReceiveToFinish(
                cache,
                cache.SendReceiveAsync(new[] { group1, group2, group3 }, true, CancellationToken.None, observer),
                observer);
            observer.AssertNoExceptions();

            group1 = Refresh(cache, group1);
            int group1Count = TestArticles(
                group1, group1.GetOriginalInboundArticles(group1.InboundArticleFilter), true, false);
            cache.SaveChanges();
            Assert.IsTrue((group1Count > 0) && (group1Count <= newInboundArticlesReceiveLimit));

            group2 = Refresh(cache, group2);
            var group2CountAfter = GetArticleCount(group2);

            if (group2CountAfter == group2CountBefore)
            {
                var count = TestArticles(
                    group2, group2.GetOriginalInboundArticles(group2.InboundArticleFilter), true, true);
                Assert.AreEqual(group2CountBefore, count);
            }
            else
            {
                Assert.IsTrue(group2CountBefore < group2CountAfter);
            }
        }

        private static void TestCompaction(string dataDirectory, int group1ArticleCount)
        {
            using (var cache2 = new Cache(dataDirectory, new NullCryptoProvider(), 1))
            {
                Assert.IsTrue(cache2.ShouldCompact);

                var draftCount = cache2.GetOutboundArticleCounts()[SendStatus.Draft];
                var subject = GetUniqueString();
                var unsavedOutbound = cache2.Hosts.First().AddOutboundArticle();
                unsavedOutbound.SaveMessage(new Message() { Subject = subject });
                Assert.AreEqual(subject, unsavedOutbound.LoadMessage().Subject);
                Assert.AreEqual(subject, unsavedOutbound.Subject);

                cache2.Compact();

                Assert.AreEqual(draftCount, cache2.GetOutboundArticleCounts()[SendStatus.Draft]);
                Assert.IsTrue(GetArticleCount(cache2.Hosts.First().Groups.First()) < group1ArticleCount);
            }

            using (var cache3 = new Cache(dataDirectory, new NullCryptoProvider(), 0))
            {
                Assert.IsFalse(cache3.ShouldCompact);
                cache3.Compact();
            }

            AssertThrow<ArgumentOutOfRangeException>(
                () => new Cache(dataDirectory, new NullCryptoProvider(), -1).Dispose());
        }

        private static IHost Refresh(Cache cache, IHost host)
        {
            return cache.Hosts.Single(h => (h.Name == host.Name) && (h.Port == host.Port));
        }

        private static IGroup Refresh(Cache cache, IGroup group)
        {
            return cache.Hosts.Single(h => (h.Name == group.Host.Name) && (h.Port == group.Host.Port)).Groups.Single(
                g => g.Name == group.Name);
        }

        private static int TestArticles(
            IGroup group, IEnumerable<IInboundArticle> articles, bool mustBeReceived, bool mustBeExpandedReadWatched)
        {
            int count = 0;
            byte[] buffer = new byte[1024];

            foreach (var article in articles)
            {
                Assert.AreEqual(group.Name, article.Group.Name);
                Assert.IsTrue(article.Number > 0);
                Assert.IsNotNull(article.DateUtc);
                Assert.IsNotNull(article.From);
                Assert.IsNotNull(article.Subject);
                Assert.AreEqual(mustBeReceived, article.HasFullArticleBeenReceived);
                Assert.AreEqual(mustBeExpandedReadWatched ? false : (bool?)null, article.HasAttachments);
                Assert.AreEqual(mustBeExpandedReadWatched, article.IsBeingWatched);
                Assert.AreEqual(mustBeExpandedReadWatched, article.IsExpanded);
                Assert.AreEqual(mustBeExpandedReadWatched, article.HasBeenRead);
                Assert.IsFalse(article.IsSelected);
                article.IsSelected = true;
                Assert.IsTrue(article.IsSelected);
                article.IsSelected = false;
                Assert.IsFalse(article.IsSelected);

                using (var content = article.GetContent())
                {
                    int total = 0;
                    int read;

                    while ((read = content.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        total += read;
                    }

                    Assert.AreEqual(mustBeReceived, total > 0);
                }

                count += TestArticles(
                    group, article.GetFollowupInboundArticles(), mustBeReceived, mustBeExpandedReadWatched);

                article.HasAttachments = false;
                article.IsBeingWatched = true;
                article.IsExpanded = true;
                article.HasBeenRead = true;

                ++count;
            }

            return count;
        }

        private static int GetArticleCount(IGroup group)
        {
            return GetCount(group.GetOriginalInboundArticles(InboundArticleFilter.NullFilter));
        }

        private static int GetCount(IEnumerable<IInboundArticle> articles)
        {
            int count = 0;

            foreach (var article in articles)
            {
                count += GetCount(article.GetFollowupInboundArticles()) + 1;
            }

            return count;
        }

        private static string CreateUniqueDirectory(FileIOProvider provider)
        {
            var directory = GetUniqueString();
            provider.CreateDirectory(directory);
            return directory;
        }

        /// <summary>This class is used to add additional entries to the Root table for testing.</summary>
        [Table(Name = "Root")]
        [SuppressMessage("Microsoft.StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "The order of the properties defines the order of database columns.")]
        private sealed class FakeRoot
        {
            [Column(IsPrimaryKey = true, IsDbGenerated = true)]
            private int Id { get; set; }

            [Column(UpdateCheck = UpdateCheck.Never)]
            internal int InsertsAndDeletesSinceLastCompaction { get; set; }
        }
    }
}
